figma-metadata-extractor 1.0.2 → 1.0.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +36 -1
- package/dist/index.cjs +97 -2
- package/dist/index.js +97 -2
- package/dist/lib.d.ts +15 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -10,8 +10,28 @@ npm install figma-metadata-extractor
|
|
|
10
10
|
|
|
11
11
|
## Quick Start
|
|
12
12
|
|
|
13
|
+
### Get Metadata with Auto-Downloaded Images (LLM-Ready!)
|
|
14
|
+
|
|
13
15
|
```typescript
|
|
14
|
-
import { getFigmaMetadata
|
|
16
|
+
import { getFigmaMetadata } from 'figma-metadata-extractor';
|
|
17
|
+
|
|
18
|
+
// Extract metadata AND automatically download image assets
|
|
19
|
+
const metadata = await getFigmaMetadata(
|
|
20
|
+
'https://figma.com/file/ABC123/My-Design',
|
|
21
|
+
{
|
|
22
|
+
apiKey: 'your-figma-api-key',
|
|
23
|
+
outputFormat: 'object',
|
|
24
|
+
downloadImages: true, // Auto-download image assets
|
|
25
|
+
localPath: './assets/images' // Where to save images
|
|
26
|
+
}
|
|
27
|
+
);
|
|
28
|
+
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
### Get Metadata Only (No Downloads)
|
|
32
|
+
|
|
33
|
+
```typescript
|
|
34
|
+
import { getFigmaMetadata } from 'figma-metadata-extractor';
|
|
15
35
|
|
|
16
36
|
// Extract metadata from a Figma file
|
|
17
37
|
const metadata = await getFigmaMetadata(
|
|
@@ -77,9 +97,24 @@ Extracts comprehensive metadata from a Figma file including layout, content, vis
|
|
|
77
97
|
- `useOAuth?: boolean` - Whether to use OAuth instead of API key
|
|
78
98
|
- `outputFormat?: 'json' | 'yaml' | 'object'` - Output format (default: 'object')
|
|
79
99
|
- `depth?: number` - Maximum depth to traverse the node tree
|
|
100
|
+
- `downloadImages?: boolean` - Automatically download image assets and enrich metadata (default: false)
|
|
101
|
+
- `localPath?: string` - Local path for downloaded images (required if downloadImages is true)
|
|
102
|
+
- `imageFormat?: 'png' | 'svg'` - Image format for downloads (default: 'png')
|
|
103
|
+
- `pngScale?: number` - Export scale for PNG images (default: 2)
|
|
80
104
|
|
|
81
105
|
**Returns:** Promise<FigmaMetadataResult | string>
|
|
82
106
|
|
|
107
|
+
When `downloadImages` is true, nodes with image assets will include a `downloadedImage` property:
|
|
108
|
+
```typescript
|
|
109
|
+
{
|
|
110
|
+
filePath: string; // Absolute path
|
|
111
|
+
relativePath: string; // Relative path for code
|
|
112
|
+
dimensions: { width, height };
|
|
113
|
+
markdown: string; // 
|
|
114
|
+
html: string; // <img src="..." />
|
|
115
|
+
}
|
|
116
|
+
```
|
|
117
|
+
|
|
83
118
|
### `downloadFigmaImages(figmaUrl, nodes, options)`
|
|
84
119
|
|
|
85
120
|
Downloads SVG and PNG images from a Figma file.
|
package/dist/index.cjs
CHANGED
|
@@ -1365,7 +1365,18 @@ function collapseSvgContainers(node, result, children) {
|
|
|
1365
1365
|
return children;
|
|
1366
1366
|
}
|
|
1367
1367
|
async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
1368
|
-
const {
|
|
1368
|
+
const {
|
|
1369
|
+
apiKey,
|
|
1370
|
+
oauthToken,
|
|
1371
|
+
useOAuth = false,
|
|
1372
|
+
outputFormat = "object",
|
|
1373
|
+
depth,
|
|
1374
|
+
downloadImages = false,
|
|
1375
|
+
localPath,
|
|
1376
|
+
imageFormat = "png",
|
|
1377
|
+
pngScale = 2,
|
|
1378
|
+
useRelativePaths = true
|
|
1379
|
+
} = options;
|
|
1369
1380
|
if (!apiKey && !oauthToken) {
|
|
1370
1381
|
throw new Error("Either apiKey or oauthToken is required");
|
|
1371
1382
|
}
|
|
@@ -1399,11 +1410,33 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1399
1410
|
`Successfully extracted data: ${simplifiedDesign.nodes.length} nodes, ${Object.keys(simplifiedDesign.globalVars?.styles || {}).length} styles`
|
|
1400
1411
|
);
|
|
1401
1412
|
const { nodes, globalVars, ...metadata } = simplifiedDesign;
|
|
1402
|
-
|
|
1413
|
+
let result = {
|
|
1403
1414
|
metadata,
|
|
1404
1415
|
nodes,
|
|
1405
1416
|
globalVars
|
|
1406
1417
|
};
|
|
1418
|
+
if (downloadImages) {
|
|
1419
|
+
if (!localPath) {
|
|
1420
|
+
throw new Error("localPath is required when downloadImages is true");
|
|
1421
|
+
}
|
|
1422
|
+
Logger.log("Discovering and downloading image assets...");
|
|
1423
|
+
const imageAssets = findImageAssets(nodes, globalVars);
|
|
1424
|
+
Logger.log(`Found ${imageAssets.length} image assets to download`);
|
|
1425
|
+
if (imageAssets.length > 0) {
|
|
1426
|
+
const imageNodes = imageAssets.map((asset) => ({
|
|
1427
|
+
nodeId: asset.id,
|
|
1428
|
+
fileName: sanitizeFileName(asset.name) + `.${imageFormat}`
|
|
1429
|
+
}));
|
|
1430
|
+
const downloadResults = await figmaService.downloadImages(
|
|
1431
|
+
fileKey,
|
|
1432
|
+
localPath,
|
|
1433
|
+
imageNodes,
|
|
1434
|
+
{ pngScale: imageFormat === "png" ? pngScale : void 0 }
|
|
1435
|
+
);
|
|
1436
|
+
result.nodes = enrichNodesWithImages(nodes, imageAssets, downloadResults, useRelativePaths);
|
|
1437
|
+
Logger.log(`Successfully downloaded and enriched ${downloadResults.length} images`);
|
|
1438
|
+
}
|
|
1439
|
+
}
|
|
1407
1440
|
if (outputFormat === "json") {
|
|
1408
1441
|
return JSON.stringify(result, null, 2);
|
|
1409
1442
|
} else if (outputFormat === "yaml") {
|
|
@@ -1497,6 +1530,68 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
|
|
|
1497
1530
|
throw new Error(`Failed to download frame image: ${error instanceof Error ? error.message : String(error)}`);
|
|
1498
1531
|
}
|
|
1499
1532
|
}
|
|
1533
|
+
function sanitizeFileName(name) {
|
|
1534
|
+
return name.replace(/[^a-z0-9]/gi, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
1535
|
+
}
|
|
1536
|
+
function findImageAssets(nodes, globalVars) {
|
|
1537
|
+
const images = [];
|
|
1538
|
+
function traverse(node) {
|
|
1539
|
+
const isImageAsset = node.type === "IMAGE-SVG" || hasImageFill(node, globalVars);
|
|
1540
|
+
if (isImageAsset) {
|
|
1541
|
+
images.push(node);
|
|
1542
|
+
}
|
|
1543
|
+
if (node.children && Array.isArray(node.children)) {
|
|
1544
|
+
node.children.forEach(traverse);
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
nodes.forEach(traverse);
|
|
1548
|
+
return images;
|
|
1549
|
+
}
|
|
1550
|
+
function hasImageFill(node, globalVars) {
|
|
1551
|
+
if (!node.fills || typeof node.fills !== "string") {
|
|
1552
|
+
return false;
|
|
1553
|
+
}
|
|
1554
|
+
const fillData = globalVars?.styles?.[node.fills];
|
|
1555
|
+
if (!fillData || !Array.isArray(fillData)) {
|
|
1556
|
+
return false;
|
|
1557
|
+
}
|
|
1558
|
+
return fillData.some((fill) => fill?.type === "IMAGE");
|
|
1559
|
+
}
|
|
1560
|
+
function enrichNodesWithImages(nodes, imageAssets, downloadResults, useRelativePaths = true) {
|
|
1561
|
+
const imageMap = /* @__PURE__ */ new Map();
|
|
1562
|
+
imageAssets.forEach((asset, index) => {
|
|
1563
|
+
const result = downloadResults[index];
|
|
1564
|
+
if (result) {
|
|
1565
|
+
let pathForMarkup;
|
|
1566
|
+
if (useRelativePaths === false) {
|
|
1567
|
+
pathForMarkup = result.filePath;
|
|
1568
|
+
} else if (typeof useRelativePaths === "string") {
|
|
1569
|
+
pathForMarkup = result.filePath.replace(useRelativePaths, ".");
|
|
1570
|
+
} else {
|
|
1571
|
+
pathForMarkup = result.filePath.replace(process.cwd(), ".");
|
|
1572
|
+
}
|
|
1573
|
+
imageMap.set(asset.id, {
|
|
1574
|
+
filePath: result.filePath,
|
|
1575
|
+
relativePath: pathForMarkup,
|
|
1576
|
+
dimensions: result.finalDimensions,
|
|
1577
|
+
wasCropped: result.wasCropped,
|
|
1578
|
+
markdown: ``,
|
|
1579
|
+
html: `<img src="${pathForMarkup}" alt="${asset.name}" width="${result.finalDimensions.width}" height="${result.finalDimensions.height}">`
|
|
1580
|
+
});
|
|
1581
|
+
}
|
|
1582
|
+
});
|
|
1583
|
+
function enrichNode(node) {
|
|
1584
|
+
const enriched = { ...node };
|
|
1585
|
+
if (imageMap.has(node.id)) {
|
|
1586
|
+
enriched.downloadedImage = imageMap.get(node.id);
|
|
1587
|
+
}
|
|
1588
|
+
if (node.children && Array.isArray(node.children)) {
|
|
1589
|
+
enriched.children = node.children.map(enrichNode);
|
|
1590
|
+
}
|
|
1591
|
+
return enriched;
|
|
1592
|
+
}
|
|
1593
|
+
return nodes.map(enrichNode);
|
|
1594
|
+
}
|
|
1500
1595
|
exports.allExtractors = allExtractors;
|
|
1501
1596
|
exports.collapseSvgContainers = collapseSvgContainers;
|
|
1502
1597
|
exports.componentExtractor = componentExtractor;
|
package/dist/index.js
CHANGED
|
@@ -1363,7 +1363,18 @@ function collapseSvgContainers(node, result, children) {
|
|
|
1363
1363
|
return children;
|
|
1364
1364
|
}
|
|
1365
1365
|
async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
1366
|
-
const {
|
|
1366
|
+
const {
|
|
1367
|
+
apiKey,
|
|
1368
|
+
oauthToken,
|
|
1369
|
+
useOAuth = false,
|
|
1370
|
+
outputFormat = "object",
|
|
1371
|
+
depth,
|
|
1372
|
+
downloadImages = false,
|
|
1373
|
+
localPath,
|
|
1374
|
+
imageFormat = "png",
|
|
1375
|
+
pngScale = 2,
|
|
1376
|
+
useRelativePaths = true
|
|
1377
|
+
} = options;
|
|
1367
1378
|
if (!apiKey && !oauthToken) {
|
|
1368
1379
|
throw new Error("Either apiKey or oauthToken is required");
|
|
1369
1380
|
}
|
|
@@ -1397,11 +1408,33 @@ async function getFigmaMetadata(figmaUrl, options = {}) {
|
|
|
1397
1408
|
`Successfully extracted data: ${simplifiedDesign.nodes.length} nodes, ${Object.keys(simplifiedDesign.globalVars?.styles || {}).length} styles`
|
|
1398
1409
|
);
|
|
1399
1410
|
const { nodes, globalVars, ...metadata } = simplifiedDesign;
|
|
1400
|
-
|
|
1411
|
+
let result = {
|
|
1401
1412
|
metadata,
|
|
1402
1413
|
nodes,
|
|
1403
1414
|
globalVars
|
|
1404
1415
|
};
|
|
1416
|
+
if (downloadImages) {
|
|
1417
|
+
if (!localPath) {
|
|
1418
|
+
throw new Error("localPath is required when downloadImages is true");
|
|
1419
|
+
}
|
|
1420
|
+
Logger.log("Discovering and downloading image assets...");
|
|
1421
|
+
const imageAssets = findImageAssets(nodes, globalVars);
|
|
1422
|
+
Logger.log(`Found ${imageAssets.length} image assets to download`);
|
|
1423
|
+
if (imageAssets.length > 0) {
|
|
1424
|
+
const imageNodes = imageAssets.map((asset) => ({
|
|
1425
|
+
nodeId: asset.id,
|
|
1426
|
+
fileName: sanitizeFileName(asset.name) + `.${imageFormat}`
|
|
1427
|
+
}));
|
|
1428
|
+
const downloadResults = await figmaService.downloadImages(
|
|
1429
|
+
fileKey,
|
|
1430
|
+
localPath,
|
|
1431
|
+
imageNodes,
|
|
1432
|
+
{ pngScale: imageFormat === "png" ? pngScale : void 0 }
|
|
1433
|
+
);
|
|
1434
|
+
result.nodes = enrichNodesWithImages(nodes, imageAssets, downloadResults, useRelativePaths);
|
|
1435
|
+
Logger.log(`Successfully downloaded and enriched ${downloadResults.length} images`);
|
|
1436
|
+
}
|
|
1437
|
+
}
|
|
1405
1438
|
if (outputFormat === "json") {
|
|
1406
1439
|
return JSON.stringify(result, null, 2);
|
|
1407
1440
|
} else if (outputFormat === "yaml") {
|
|
@@ -1495,6 +1528,68 @@ async function downloadFigmaFrameImage(figmaUrl, options) {
|
|
|
1495
1528
|
throw new Error(`Failed to download frame image: ${error instanceof Error ? error.message : String(error)}`);
|
|
1496
1529
|
}
|
|
1497
1530
|
}
|
|
1531
|
+
function sanitizeFileName(name) {
|
|
1532
|
+
return name.replace(/[^a-z0-9]/gi, "-").replace(/-+/g, "-").replace(/^-|-$/g, "").toLowerCase();
|
|
1533
|
+
}
|
|
1534
|
+
function findImageAssets(nodes, globalVars) {
|
|
1535
|
+
const images = [];
|
|
1536
|
+
function traverse(node) {
|
|
1537
|
+
const isImageAsset = node.type === "IMAGE-SVG" || hasImageFill(node, globalVars);
|
|
1538
|
+
if (isImageAsset) {
|
|
1539
|
+
images.push(node);
|
|
1540
|
+
}
|
|
1541
|
+
if (node.children && Array.isArray(node.children)) {
|
|
1542
|
+
node.children.forEach(traverse);
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
nodes.forEach(traverse);
|
|
1546
|
+
return images;
|
|
1547
|
+
}
|
|
1548
|
+
function hasImageFill(node, globalVars) {
|
|
1549
|
+
if (!node.fills || typeof node.fills !== "string") {
|
|
1550
|
+
return false;
|
|
1551
|
+
}
|
|
1552
|
+
const fillData = globalVars?.styles?.[node.fills];
|
|
1553
|
+
if (!fillData || !Array.isArray(fillData)) {
|
|
1554
|
+
return false;
|
|
1555
|
+
}
|
|
1556
|
+
return fillData.some((fill) => fill?.type === "IMAGE");
|
|
1557
|
+
}
|
|
1558
|
+
function enrichNodesWithImages(nodes, imageAssets, downloadResults, useRelativePaths = true) {
|
|
1559
|
+
const imageMap = /* @__PURE__ */ new Map();
|
|
1560
|
+
imageAssets.forEach((asset, index) => {
|
|
1561
|
+
const result = downloadResults[index];
|
|
1562
|
+
if (result) {
|
|
1563
|
+
let pathForMarkup;
|
|
1564
|
+
if (useRelativePaths === false) {
|
|
1565
|
+
pathForMarkup = result.filePath;
|
|
1566
|
+
} else if (typeof useRelativePaths === "string") {
|
|
1567
|
+
pathForMarkup = result.filePath.replace(useRelativePaths, ".");
|
|
1568
|
+
} else {
|
|
1569
|
+
pathForMarkup = result.filePath.replace(process.cwd(), ".");
|
|
1570
|
+
}
|
|
1571
|
+
imageMap.set(asset.id, {
|
|
1572
|
+
filePath: result.filePath,
|
|
1573
|
+
relativePath: pathForMarkup,
|
|
1574
|
+
dimensions: result.finalDimensions,
|
|
1575
|
+
wasCropped: result.wasCropped,
|
|
1576
|
+
markdown: ``,
|
|
1577
|
+
html: `<img src="${pathForMarkup}" alt="${asset.name}" width="${result.finalDimensions.width}" height="${result.finalDimensions.height}">`
|
|
1578
|
+
});
|
|
1579
|
+
}
|
|
1580
|
+
});
|
|
1581
|
+
function enrichNode(node) {
|
|
1582
|
+
const enriched = { ...node };
|
|
1583
|
+
if (imageMap.has(node.id)) {
|
|
1584
|
+
enriched.downloadedImage = imageMap.get(node.id);
|
|
1585
|
+
}
|
|
1586
|
+
if (node.children && Array.isArray(node.children)) {
|
|
1587
|
+
enriched.children = node.children.map(enrichNode);
|
|
1588
|
+
}
|
|
1589
|
+
return enriched;
|
|
1590
|
+
}
|
|
1591
|
+
return nodes.map(enrichNode);
|
|
1592
|
+
}
|
|
1498
1593
|
export {
|
|
1499
1594
|
allExtractors,
|
|
1500
1595
|
collapseSvgContainers,
|
package/dist/lib.d.ts
CHANGED
|
@@ -9,6 +9,21 @@ export interface FigmaMetadataOptions {
|
|
|
9
9
|
outputFormat?: "json" | "yaml" | "object";
|
|
10
10
|
/** Maximum depth to traverse the node tree */
|
|
11
11
|
depth?: number;
|
|
12
|
+
/** Automatically download image assets and enrich metadata with file paths */
|
|
13
|
+
downloadImages?: boolean;
|
|
14
|
+
/** Local path for downloaded images (required if downloadImages is true) */
|
|
15
|
+
localPath?: string;
|
|
16
|
+
/** Image format for downloads (defaults to 'png') */
|
|
17
|
+
imageFormat?: 'png' | 'svg';
|
|
18
|
+
/** Export scale for PNG images (defaults to 2) */
|
|
19
|
+
pngScale?: number;
|
|
20
|
+
/**
|
|
21
|
+
* Use relative paths in downloadedImage properties instead of absolute paths.
|
|
22
|
+
* If true, paths will be relative to process.cwd().
|
|
23
|
+
* If a string, paths will be relative to that base path.
|
|
24
|
+
* Default: true
|
|
25
|
+
*/
|
|
26
|
+
useRelativePaths?: boolean | string;
|
|
12
27
|
}
|
|
13
28
|
export interface FigmaImageOptions {
|
|
14
29
|
/** Export scale for PNG images (defaults to 2) */
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "figma-metadata-extractor",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.4",
|
|
4
4
|
"description": "Extract metadata and download images from Figma files. A standalone library for accessing Figma design data and downloading frame images programmatically.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.cjs",
|